Skip to content

0.26.0: the runner leaves the consumer's repository - #95

Merged
spscream merged 12 commits into
mainfrom
feat/am/dispatcher-without-copy
Sep 25, 2026
Merged

spscream merged 12 commits into
mainfrom
feat/am/dispatcher-without-copy

Conversation

@spscream

@spscream spscream commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Why

.floppy/run existed for one reason: a skill had no way to say where the
plugin was, so something inside the consumer's repository had to search for it.
It does have a way. A harness states the skill's own base directory above the
skill text — Base directory for this skill: <plugin>/skills/workstatus —
measured in Claude Code on 2026-09-22, on this plugin and one other. With that
line, the plugin directory is two levels up and the copy has no job left.

The copy was not free. It was 62 tracked files and 497 lines naming a path
that only exists after an init; it went stale silently in a second clone; and
it is what made AI_FLOPPY_HOME necessary, because a run could otherwise land
in a different copy of the same repository and report a script you had just
fixed as still broken.

What changed

In the order they had to happen — step 1 before step 2, or step 2 ships six
skills that stop at scripts/run:36 with FLOPPY_ROOT: unbound variable:

  1. scripts/run roots itself in ${BASH_SOURCE[0]} instead of being
    handed FLOPPY_ROOT by the shim, and exports FLOPPY_RUN for the verbs
    that print a next command. A direct call needs no floppy variable at all.
  2. Six SKILL.md files stop naming .floppy/run. Each states where
    <plugin> comes from — the base-directory line the harness prints — and
    calls bash <plugin>/scripts/run <verb>. Each also says what to do when
    that line is absent: try $CLAUDE_PLUGIN_ROOT / $CURSOR_PLUGIN_ROOT,
    then ask, and never guess a cache path.
  3. skills/init/SKILL.md drops its 33-line hand copy of the plugin search,
    which existed because init ran before .floppy/run existed.
  4. init writes .floppy/config and nothing else. No cp, and its
    x no shim at …/shim/run refusal (rc 2) goes with the copy it guarded. It
    gains two migration reminders instead: one when AGENTS.md still names
    .floppy/run, one when the leftover file is still in the repository, with
    the git rm that drops it. It never removes the file itself.
  5. .floppy/run stops being tracked here, and the eleven test files that
    built their sandbox by copying the shim now call the dispatcher directly —
    which is also what covers the self-rooting.
  6. 0.26.0: three manifests and a changelog entry carrying the migration.

shim/run still ships, byte for byte. A repository created before this
release keeps working: its copy still finds the plugin and still runs the verb,
nothing in the plugin calls it any more, and git rm .floppy/run is the whole
migration. The shim is deliberately left untouched because it cmps itself
against the plugin's copy on every call — any edit here would tell every one of
those repositories that their copy is stale. test-shim.sh,
test-shim-staleness.sh and test-interpreter.sh keep exercising it.

How verified

  • /bin/bash tests/run.sh — 23 files, 1038 assertions, 0 failed, rc 0.
  • python3 scripts/knowledge-recheck.py — 9 passed, 0 failed, 3 skipped
    (macOS-only claims), 14 not machine-checkable.
  • python3 scripts/translation-check.py — clean; the five changed Russian
    documents were compared against their sources' diffs and re-stamped.
  • bash scripts/run status and bash scripts/run env run with no floppy
    variable set; FLOPPY_RUN appears in env output as the dispatcher's
    absolute path.
  • git grep -c '\.floppy/run': 497 lines in 62 files → 182 in 26. What remains
    is shim/run itself, the three tests that exercise it, the migration
    reminders in init.sh and install.md, the eval scaffolds, and history
    (CHANGELOG, docs/plans/, docs/specs/). No line tells anyone to run a file
    that is not there.
  • Two independent reviewers on the full diff before publishing, on separate
    lenses — runtime behaviour, and the evidentiary strength of the tests. Eight
    findings between them; six fixed here, two accepted and recorded. What they
    found and what it cost:
    • an exported CDPATH with a relative entry made cd print into the
      substitution that derives FLOPPY_ROOT, so bash scripts/run status (the
      relative spelling CLAUDE.md documents) died in the config parser and
      blamed the install. Measured 3/3; unset CDPATH in the dispatcher and in
      the verbs that build their own hint.
    • the fallback hint did not quote a plugin path with a space in it.
    • the "harness prints the base directory" fallback was written in init
      alone, so a silent harness took down the whole ritual, not one verb.
    • the guard that replaced tests/test-init-bootstrap.sh passed <plugin>/run
      and "one directory above". It now resolves every <plugin>/… path a skill
      names against this checkout and asserts the distance.
    • init's AGENTS.md reminder claimed more than its grep knew, and skipped
      the case where the stale line sits outside the section.
    • three claims were wrong: "works with an empty environment" (the config
      parser reads $HOME), "the only file the plugin puts in .floppy/"
      (heat writes heat.log there), and a widened eval grader that read like
      a fix.
    • tests/test-dispatcher.sh is new, for what nothing was watching:
      self-rooting, refusing an inherited FLOPPY_ROOT, the CDPATH regression,
      and both spellings of the hint. Reverting either fix turns three of its
      assertions red.

What is out, and what was risked

The human-typed command is dropped, not replaced. The open question in the
brief was what a person types now that no short path exists. The answer is that
there was nothing to replace: the operator of every repository using floppy
reports never having typed .floppy/run by hand (2026-09-25). No
AI_FLOPPY_HOME recipe and no cache path with a version in its last segment is
invented to stand in for it.

Consumers that call .floppy/run from a hook or from CI would break. They
were measured, and there are none. .floppy/ exists in seven repositories; the
mentions are prose in documents, not calls:

repository files naming .floppy/run where
effectssdk 23 AGENTS.md (8 lines) + status documents
agents_harness 2 AGENTS.md (2 lines), .agent-memory/MEMORY.md
mcu_playground 2 AGENTS.md (1 line), docs/statuses/NOW.md
vps_inventory 2 AGENTS.md (2 lines), .agent-memory/quota.lock
malaev.dev 1 AGENTS.md (1 line)
fleet 1 docs/statuses/NOW.md
ai_floppy — migrated by this pull request

No hook and no workflow in any of them calls it. Fixing those six is not part
of this pull request; the line they should write instead is the one AGENTS.md
here now carries:

Its verbs are run from the plugin, not from here:
bash <plugin>/scripts/run <verb>, where <plugin> is two directories above
the base directory the harness states when it loads a floppy skill
(Base directory for this skill: <plugin>/skills/start).

What is assumed rather than measured. The base-directory line is measured
in Claude Code at the top level of a session. It is not verified in Cursor,
nor inside a subagent, nor in a resumed session. That is precisely why
shim/run stays: if one of those turns out not to state a base directory, the
fallback is a copy that searches, and it is still shipped. Every skill now also
says what to do when the line is absent, instead of only init.

The eval scaffolds now have an unreachable oracle. evals/ invents fixture
state at .floppy/run; with the skills calling <plugin>/scripts/run, that
stand-in is no longer in the path of the call, so the case is not merely
unchanged but unpassable. The brief scoped this pull request to one grader
regex under evals/, so the gap is recorded at the edit and in
evals/README.md — naming .floppy/workstatus-project.sh as the seam a real
fixture would use — rather than fixed here. The cases are unrun by decision
(2026-09-17).

bash 3.2 was settled by CI, not locally. Only GNU bash 5.1 was available
here, which also makes tests/test-interpreter.sh skip its two-hop check. The
macos-bash-3-2 job on this pull request is green. No bash 4+ construct was
introduced — the touched files were scanned for declare -A, mapfile,
${var^^}, &>>, wait -n and GNU-only flags.

`scripts/run` derived every path from an exported FLOPPY_ROOT, which only the
shim ever set. Measured 2026-09-22: a bare `bash <plugin>/scripts/run status`
stopped at line 36 with `FLOPPY_ROOT: unbound variable` — a raw bash error, and
the reason every skill had to go through the consumer's copy of the shim.

The file whose location is wanted is the file being executed, so the root is
now derived from `${BASH_SOURCE[0]}` — unconditionally, not just when the
variable is unset: a FLOPPY_ROOT naming another copy of the plugin would send
every verb into that copy's scripts while this one dispatches, which is the
"a fixed script reported as still broken" failure shim/run's header records.

It also exports FLOPPY_RUN, the absolute spelling of itself, for the hints the
verbs print. Those name `.floppy/run` today, a path that is about to stop
existing; a hint has to be pasteable from wherever the reader is standing.
Every skill is handed its own absolute base directory when the harness loads
it — measured 2026-09-22, twice, in-band above the skill text: "Base directory
for this skill: <plugin>/skills/workstatus". The plugin root is its
grandparent, which is the whole of what the consumer's copy of the shim was
computing. So the five rites now spell the call `<plugin>/scripts/run <verb>`
and each says once where `<plugin>` comes from.

The named assumption, because it is not measured: that the root is STATED to
the agent was. That the agent then substitutes it correctly every time,
instead of typing the `.floppy/run` it has read everywhere else, was not. That
substitution is the whole risk here.

`evals/workstatus-checks-instead-of-recalling`'s grader matched the command by
the literal `.floppy/run …status`, so it graded the spelling of a path rather
than the act of running the verb. Widened to accept both spellings; the
fixtures are untouched, since their stand-ins are oracles for an invented
state and not a way around `$HOME`. What that leaves open — the oracle now
sits at a path the skills no longer name — is written into evals/README.md
where the next person to run these cases will read it.
Step 2 carried 33 lines reproducing the shim's six-way search, because `init`
runs before the repository holds anything that could do the finding. It is the
one place where that copy is now provably redundant: the harness states the
skill's base directory at load, so the plugin root is two directories up from
a string already on screen — no cache glob, no `sort -V`, no Cursor SHA
ordering. The copy had been wrong once already, carrying four of six branches
for as long as it existed (found 2026-09-09).

tests/test-init-bootstrap.sh existed to extract that block and run it against
each branch. With no block there is nothing to extract, so it goes, and what
replaces it in tests/test-skills.sh guards what is left to get wrong: no skill
may name `.floppy/run`, and a skill using the `<plugin>` placeholder has to say
where it comes from. Both fired on the first run — two skills wrapped the
base-directory line across two lines — which is the only evidence that a
structural guard works.
The runtime hints were written when every consumer had `.floppy/run`: nine
scripts told the reader to run `bash .floppy/run store` or `... lock release`.
With the copy gone there is no short path to print, and a hint naming a file
that is not there is worse than no hint.

Each script now prints the dispatcher's own absolute path. `scripts/run`
exports FLOPPY_RUN alongside FLOPPY_ROOT; a script reached directly — as the
tests reach them — falls back to deriving it from its own directory, so the
hint is correct either way.

The header comments, which are read rather than run, use the `<plugin>`
spelling the skills use.
This is what the three commits before it were for. `init` copied `shim/run`
into the consumer as `.floppy/run` and refused to run at all when the plugin
had no shim to copy; nothing calls that copy any more, so both go. What `init`
puts in `.floppy/` is `config`: the consumer's repository carries data, not
code.

Two things are added rather than removed, because a repository created before
today still has the file:

- when the `AGENTS.md` section it maintains still names `.floppy/run` as the
  entry point, `init` says so and gives the line to write instead;
- when `.floppy/run` is still in the repository, the last thing `init` prints
  is what it is and the `git rm` that drops it. It does not remove the file
  itself — it is committed, and something of the reader's may still call it.

The generated config and `quota.lock` header name verbs (`floppy's "store"
verb`) where they used to name a path.

`tests/test-init.sh` asserts the inverse of what it asserted before: no runner
is placed, and the file list `init` reports no longer carries one.
Eleven test files built their sandbox by copying `shim/run` into
`<sandbox>/.floppy/run` and then invoking it with `AI_FLOPPY_HOME` set —
roughly 180 call sites exercising the path a consumer no longer has. They now
call `bash "$ROOT/scripts/run"`, the way a skill does, which is also what makes
the self-rooting in `scripts/run` covered by something other than one manual
run.

Three files keep the old shape on purpose, because the shim still ships and
still has to work: `test-shim.sh`, `test-shim-staleness.sh` and
`test-interpreter.sh`.
floppy is installed in floppy, so the plugin's own `.floppy/run` is the first
consumer to migrate: `git rm`, and `watched_files` loses the path it can no
longer watch. `.floppy/` here is `config` and nothing else, which is what
`init` now produces elsewhere.

The config's comments name verbs rather than the command line that used to run
them.
`docs/guide/install.md` had a section called "The shim file in your
repository", most of it about a copy that can go stale; it is now "How a
command is run" — the path, where `<plugin>` comes from, and the `git rm` plus
the `AGENTS.md` line for a repository that has the file. The guide to config
and the guide to skills name verbs and `<plugin>/scripts/run` where they named
`.floppy/run`; `README.md` stops promising a file `init` no longer writes.

`AGENTS.md` here writes the line every migrating consumer needs in theirs.

`CLAUDE.md` gets the architecture as it now is — two layers, `shim/run` kept
and deliberately byte-identical, because it `cmp`s itself against the plugin's
copy and any edit would report every legacy consumer's copy as stale.

`docs/statuses/NOW.md` restates the frozen decision it was recording. "A
committed copy, not a generated file" was the answer to a gitignored shim; the
decision behind it — a runner in the consumer's repository is either committed
or absent, never gitignored — is what this release carries out, by removing it
rather than generating it.

The five Russian translations follow their sources and are re-stamped.
Three manifests and the entry that `changelog-extract.sh` turns into the
release notes. The entry answers the file's own question for the last time
that it can be asked of a new repository — there is nothing to refresh — and
carries the migration for a repository that already has the copy.

The preamble's shim question is rewritten in the past tense rather than
deleted: every entry below it was written while the copy existed, and the
question is still the right one to ask of those repositories.
Review found it and it reproduces 3/3: exported with a relative entry, CDPATH
makes `cd` print the directory it found on stdout, that lands inside the
command substitution, and FLOPPY_ROOT comes out two lines long. lib-config.sh
is then not sourced, no FLOPPY_* setting is exported, and the reader is told
"The install is incomplete — reinstall the plugin", which is not the cause.
The exposed spelling is `bash scripts/run status`, the relative one CLAUDE.md
documents; CDPATH is not consulted for an absolute path, which is why the
suite was green.

`unset CDPATH` in the dispatcher covers everything it dispatches, because the
variable is then gone from what the verbs inherit. The eight verbs that build
their own hint unset it too, for the calls that do not come through the
dispatcher.

The same review measured the other half: a plugin under a path with a space
printed `bash /some where/scripts/run heat …`, which cannot be pasted back.
The dispatcher already quoted conditionally; the fallback now quotes
unconditionally, because it is reached only by a direct call and there the
quotes cost less than a broken command.

`tests/test-dispatcher.sh` is new and covers what nothing was watching: that
the dispatcher roots itself with no variable set, that it overrides an
inherited FLOPPY_ROOT rather than honouring it (the header argued for this and
nothing held it), that CDPATH does not break the run, and that both spellings
of the hint come out pasteable. Reverting either fix turns three of its
assertions red.

`scripts/run` also gets its executable bit. It is the entry point now; the
file that used to be one had it.
Two defects from review, both in the reminders this branch added.

The AGENTS.md one greps the whole file but said "that section still names
.floppy/run", so a mention anywhere else — a watched-files list, a note —
accused a section that was already correct. It also only ran when the section
marker was present, which left the opposite case silent: a repository whose
stale line sits outside the section got a fresh section appended and no word
about the old instruction above it. The grep moves out of that branch and the
message now says what it knows: AGENTS.md still names .floppy/run.

Neither reminder had a test, though both are promised in prose — install.md
for the AGENTS.md line, the changelog for the leftover file. `test-init.sh`
now builds a pre-0.26.0 consumer and asserts both, including that init leaves
the file itself alone, and a second one for the mention outside the section.
…resolves it

Both reviewers opened with the same thing. The base-directory line is measured
in Claude Code only, the plugin also ships for Cursor, and the fallback — try
$CLAUDE_PLUGIN_ROOT or $CURSOR_PLUGIN_ROOT, then ask, never guess a cache path
— was written in `init` alone. The five daily rites just said "write that
absolute path", so a harness that does not print the line would have taken the
whole ritual down, not one verb. They now carry the same paragraph.

The guard that replaced `test-init-bootstrap.sh` was measured as weaker than
it looked: `<plugin>/run` instead of `<plugin>/scripts/run`, and "one
directory above" instead of "two", both left the suite green. Prose cannot be
executed, but a path can be resolved — every `<plugin>/…` a skill names is now
checked against this checkout, and the distance is asserted as a phrase. Both
mutations go red.

Three claims corrected while the reviewers' measurements were still in hand:

- the changelog said a direct call "works with an empty environment"; under
  `env -i` it prints two unbound-variable errors from the config parser, which
  has always read `$HOME`. It needs no *floppy* variable, which is the claim
  worth making.
- `install.md` called `config` the only file the plugin puts in `.floppy/`.
  `heat` writes `.floppy/heat.log` there. It is the only file *init* puts
  there, and the only one committed.
- the eval grader was widened to accept both spellings, which reads like the
  case was fixed. It is not: the skills call the real dispatcher, so the
  stand-in the scaffold writes is never reached and the invented fact is never
  printed. Said at the edit, not only in `evals/README.md`.
@spscream
spscream merged commit 799e532 into main Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant